/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.csm.sim2k11.simulator;

import com.csm.sim2k11.entities.Client;
import com.csm.sim2k11.entities.Restaurant;
import com.csm.sim2k11.entities.Table;
import com.csm.sim2k11.frontend.MainWindow;
import java.util.List;
import javax.swing.SwingWorker;
import javax.swing.table.DefaultTableModel;

/**
 *  
 * @author Santiago Moreno
 */
public class Simulator extends SwingWorker<Void, Integer> {

    //CONSTANTS
    public final static int BIGIN = 0;
    public final static int CLIENT_ARRIVAL = 1;
    public final static int FINISH_TAKING_ORDER = 2;
    public final static int FINISH_COOKING = 3;
    public final static int FINISH_DELIVERING_ORDER = 4;
    public final static int FINISH_LUNCH = 5;
    //ATTRIBUTES
    private int actualIterations;
    private DefaultTableModel tableModel;
    private double watch, rndQuantity;
    private Restaurant restaurant;
    private double[] eventsTime;
    private int contClientsLost, contClients, contClientsGroup;
    private long time;
    private MainWindow window;

    //CONSTRUCTOR
    public Simulator(MainWindow window) {
        this.window = window;
        actualIterations = 0;
        watch = 0d;
        eventsTime = new double[7];
    }

    //STATIC METHODS
    /**
     * Returns the percentage complete of the simulation
     * @return Double between 1 and 0.
     */
    public double getPercentageComplete() {
        return (double) actualIterations / Configuration.getCantSimulations();
    }

    private void checkGarbage(int actualSimulations) {
        if (actualSimulations >= Configuration.getCantSimulationsBeforeClean()) {
            Runtime garbageCollector = Runtime.getRuntime();
            garbageCollector.gc();
        }
    }

    //SIMULATION METHODS
    /**
     * Controls the conditions for finishing the simulation
     * @return False if the simulation finish. True if the simulation continues.
     */
    private boolean finishSimulation() {
        /*
         * If the method "stopSimulation()" wasn't called, and the actual
         * simulation iteration is lower that the max iteration available,
         * the simulation continues.
         */
        return !isCancelled() && (this.actualIterations
                < Configuration.getCantSimulations());
    }

    /**
     * 
     * @return 
     */
    private int nextEvent() {
        double next = Double.MAX_VALUE;
        double second = Double.MAX_VALUE;
        int soonest = -1;
        int secondSoonest = -1;
        for (int i = 0; i < eventsTime.length; i++) {
            if (eventsTime[i] < next) {
                second = next;
                secondSoonest = soonest;
                next = eventsTime[i];
                soonest = i;
            }
        }
        //Check the priority
        if (next == second) {
            soonest = checkTablePriority(soonest, secondSoonest);
        }
        return soonest;
    }

    /**
     * @return the contClientsLost
     */
    public int getContClientsLost() {
        return contClientsLost;
    }

    /**
     * @return the contClients
     */
    public int getContClients() {
        return contClients;
    }

    private String getPrittyRndQuantity() {
        if (rndQuantity > 0) {
            return String.valueOf(rndQuantity);
        } else {
            return "--";
        }
    }

    private String getPrittyClientsQuantity() {
        if (rndQuantity > 0) {
            return String.valueOf(restaurant.getClientsQuantity(rndQuantity));
        } else {
            return "--";
        }
    }

    @Override
    protected Void doInBackground() throws Exception {
        window.simulationStarted();
        //Guardamos el tiempo actual
        time = System.currentTimeMillis();
        contClients = contClientsLost = contClientsGroup = 0;
        tableModel = new DefaultTableModel(new Object[][]{},
                new String[]{
                    "Orden", "Reloj", "Evento", //Datos basicos
                    "Prox. Llegada", "rnd", "Cant. Clientes", //Datos de Llegada
                    "Cola de Espera", //Cola de Espera del restaurant
                    "Estado M1", "rnd C1", "Menu C1", "rnd C2", "Menu C2",//Datos de Mesa 1
                    "rnd C3", "Menu C3", "rnd C4", "Menu C4", "rnd Comida",
                    "t Comida", "Prox. Fin M1",
                    "Estado M2", "rnd C1", "Menu C1", "rnd C2", "Menu C2",//Datos de Mesa 2
                    "rnd C3", "Menu C3", "rnd C4", "Menu C4", "rnd Comida",
                    "t Comida", "Prox. Fin M2",
                    "Estado M3", "rnd C1", "Menu C1", "rnd C2", "Menu C2",//Datos de Mesa 3
                    "rnd C3", "Menu C3", "rnd C4", "Menu C4", "rnd Comida",
                    "t Comida", "Prox. Fin M3",
                    "Estado M4", "rnd C1", "Menu C1", "rnd C2", "Menu C2",//Datos de Mesa 4
                    "rnd C3", "Menu C3", "rnd C4", "Menu C4", "rnd Comida",
                    "t Comida", "Prox. Fin M4",
                    "Estado M5", "rnd C1", "Menu C1", "rnd C2", "Menu C2",//Datos de Mesa 5
                    "rnd C3", "Menu C3", "rnd C4", "Menu C4", "rnd Comida",
                    "t Comida", "Prox. Fin M5",
                    "Estado M6", "rnd C1", "Menu C1", "rnd C2", "Menu C2",//Datos de Mesa 6
                    "rnd C3", "Menu C3", "rnd C4", "Menu C4", "rnd Comida",
                    "t Comida", "Prox. Fin M6"
                        ,"Cant. Clientes Atendidos", "Cant. Clientes Perdidos"
                }) {

            boolean[] canEdit = new boolean[]{
                false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false
            };

            public boolean isCellEditable(int rowIndex, int columnIndex) {
                return canEdit[columnIndex];
            }
        };

        restaurant = new Restaurant();

        //Llegada del proximo cliente
        eventsTime[0] = watch + restaurant.getNextClientTime();
        //Fin de Evento en Mesa 1
        eventsTime[1] = Double.MAX_VALUE;
        //Fin de Evento en Mesa 2
        eventsTime[2] = Double.MAX_VALUE;
        //Fin de Evento en Mesa 3
        eventsTime[3] = Double.MAX_VALUE;
        //Fin de Evento en Mesa 4
        eventsTime[4] = Double.MAX_VALUE;
        //Fin de Evento en Mesa 5
        eventsTime[5] = Double.MAX_VALUE;
        //Fin de Evento en Mesa 6
        eventsTime[6] = Double.MAX_VALUE;

        //Primer fila
        tableModel.addRow(new Object[]{
                    actualIterations, watch, "Inicio", //Datos basicos
                    eventsTime[0], rndQuantity, "0", //Datos de Llegada
                    "0", //Cola de Espera del restaurant
                    "Libre", "--", "--", "--", "--",//Datos de Mesa 1
                    "--", "--", "--", "--", "--",
                    "--", "--",
                    "Libre", "--", "--", "--", "--",//Datos de Mesa 2
                    "--", "--", "--", "--", "--",
                    "--", "--",
                    "Libre", "--", "--", "--", "--",//Datos de Mesa 3
                    "--", "--", "--", "--", "--",
                    "--", "--",
                    "Libre", "--", "--", "--", "--",//Datos de Mesa 4
                    "--", "--", "--", "--", "--",
                    "--", "--",
                    "Libre", "--", "--", "--", "--",//Datos de Mesa 5
                    "--", "--", "--", "--", "--",
                    "--", "--",
                    "Libre", "--", "--", "--", "--",//Datos de Mesa 6
                    "--", "--", "--", "--", "--",
                    "--", "--", "0", "0"});

        try {
            while (finishSimulation()) {
                actualIterations++;
                int nextEventActor = nextEvent();
                switch (nextEventActor) {
                    case 0:
                        //Llega un Cliente

                        //Se actualiza el Reloj
                        watch = eventsTime[0];
                        //Se calcula la llegada del proximo cliente
                        eventsTime[0] = watch + restaurant.getNextClientTime();
                        //Se crea un nuevo grupo de clientes, y se calcula la cantidad
                        rndQuantity = Math.random();
                        Client newClient = new Client(contClientsGroup, restaurant.getClientsQuantity(rndQuantity));
                        //Se suma la cantidad de clientes q llegaron al contador
                        contClients += newClient.getClientsQuantiy();
                        contClientsGroup++;
                        newClient.setID(contClientsGroup);
                        //Se chequea el estado de las mesas
                        Table table = restaurant.getFreeTable();
                        if (table != null) {
                            //Hay mesa libre
                            table.setClient(newClient);
                            //Se Simulan los pedidos de cada cliente
                            double[] rndOrders = table.simulateOrders();
                            //Y se comienza la atención
                            table.toNextState(watch);
                            eventsTime[table.getTableID()] = table.getNextFinish();
                        } else if (table == null & restaurant.getClientLine().size()
                                < Configuration.getCantCola()) {
                            //Si no hay mesa, y hay espacio en la fila de espera
                            //Se agrega al cliente/s a la fila de espera
                            restaurant.addClientToLine(newClient);
                            //Y setea la hora en q inicia a esperar
                            newClient.setStartWaiting(watch);
                        } else {
                            /*
                             * Si no hay mesas libres, y la fila ya esta llena,
                             * el cliente se va.
                             * Se suma la cant. de clientes al contador de
                             * clientes perdidos.
                             */
                            contClientsLost += newClient.getClientsQuantiy();
                            contClientsGroup++;
                        }
                        //TODO: Print the client arrival on the table model
                        addSimulationRow(CLIENT_ARRIVAL, contClientsGroup);
                        break;
                    case 1:
                    case 2:
                    case 3:
                    case 4:
                    case 5:
                    case 6:
                        //Si corresponde a alguna de las mesas

                        //Se actualiza el Reloj
                        watch = eventsTime[nextEventActor];
                        int eventTableID = nextEventActor - 1;
                        int clientID = restaurant.getTables()[eventTableID].getClient().getID();
                        int event = restaurant.getTables()[eventTableID].toNextState(watch);
                        //Si el evento corresponde a una mesa q queda libre, revisamos la cola
                        if (event == FINISH_LUNCH && restaurant.getClientLine().size() > 0) {
                            restaurant.getTables()[eventTableID].setClient(
                                    (Client) restaurant.getClientLine().remove(0));
                            restaurant.getTables()[eventTableID].toNextState(watch);
                        }
                        eventsTime[nextEventActor] = restaurant.getTables()[eventTableID].getNextFinish();
                        addSimulationRow(event, clientID);
                        break;
                }//End Switch
                publish((int) (getPercentageComplete() * 100));
                checkGarbage(actualIterations);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        //Calculamos el tiempo que demoro la simulacion
        time = System.currentTimeMillis() - time;
        return null;
    }

    @Override
    protected void process(List<Integer> progress) {
        checkMemoryState();
        window.setProgress(progress.get(progress.size() - 1));
    }

    @Override
    protected void done() {
        // Mostramos el nombre del hilo para ver que efectivamente esto
        // se ejecuta en el hilo de eventos.
        if (isCancelled()) {
            window.simulationCanceled();
        } else {
            double seconds = time / 1000d;
            window.simulationDone(contClients, contClientsLost, seconds);
            int[] simulationResults = new int[]{contClients,contClientsLost};
            Configuration.getHistoricResults().add(simulationResults);
        }
        window.updateTable(tableModel);
    }

    private String getEventName(int event) {
        switch (event) {
            case BIGIN:
                return "Inicio";
            case CLIENT_ARRIVAL:
                return "Llegada Cliente";
            case FINISH_COOKING:
                return "Fin Cocinado de Orden";
            case FINISH_DELIVERING_ORDER:
                return "Fin Entrega de Orden";
            case FINISH_LUNCH:
                return "Fin Consumo Orden";
            case FINISH_TAKING_ORDER:
                return "Fin Toma de Orden";
            default:
                return "EVENTO DESCONOCIDO!";
        }
    }

    private void addSimulationRow(int event, int actor) {
        tableModel.addRow(new Object[]{
                    actualIterations, watch, getEventName(event) + " C" + actor, //Datos basicos
                    eventsTime[0], getPrittyRndQuantity(), getPrittyClientsQuantity(), //Datos de Llegada
                    restaurant.getClientLine().size(), //Cola de Espera del restaurant
                    restaurant.getTables()[0].getPrittyState(),//Datos de Mesa 1
                    restaurant.getTables()[0].getMenuRnds()[0], restaurant.getTables()[0].getMenu()[0],
                    restaurant.getTables()[0].getMenuRnds()[1], restaurant.getTables()[0].getMenu()[1],
                    restaurant.getTables()[0].getMenuRnds()[2], restaurant.getTables()[0].getMenu()[2],
                    restaurant.getTables()[0].getMenuRnds()[3], restaurant.getTables()[0].getMenu()[3],
                    restaurant.getTables()[0].getRndLunch(), restaurant.getTables()[0].getPrettyNextEndLunch(),
                    restaurant.getTables()[0].getPrittyNextFinish(),
                    restaurant.getTables()[1].getPrittyState(),//Datos de Mesa 2
                    restaurant.getTables()[1].getMenuRnds()[0], restaurant.getTables()[1].getMenu()[0],
                    restaurant.getTables()[1].getMenuRnds()[1], restaurant.getTables()[1].getMenu()[1],
                    restaurant.getTables()[1].getMenuRnds()[2], restaurant.getTables()[1].getMenu()[2],
                    restaurant.getTables()[1].getMenuRnds()[3], restaurant.getTables()[1].getMenu()[3],
                    restaurant.getTables()[1].getRndLunch(), restaurant.getTables()[1].getPrettyNextEndLunch(),
                    restaurant.getTables()[1].getPrittyNextFinish(),
                    restaurant.getTables()[2].getPrittyState(),//Datos de Mesa 3
                    restaurant.getTables()[2].getMenuRnds()[0], restaurant.getTables()[2].getMenu()[0],
                    restaurant.getTables()[2].getMenuRnds()[1], restaurant.getTables()[2].getMenu()[1],
                    restaurant.getTables()[2].getMenuRnds()[2], restaurant.getTables()[2].getMenu()[2],
                    restaurant.getTables()[2].getMenuRnds()[3], restaurant.getTables()[2].getMenu()[3],
                    restaurant.getTables()[2].getRndLunch(), restaurant.getTables()[2].getPrettyNextEndLunch(),
                    restaurant.getTables()[2].getPrittyNextFinish(),
                    restaurant.getTables()[3].getPrittyState(),//Datos de Mesa 4
                    restaurant.getTables()[3].getMenuRnds()[0], restaurant.getTables()[3].getMenu()[0],
                    restaurant.getTables()[3].getMenuRnds()[1], restaurant.getTables()[3].getMenu()[1],
                    restaurant.getTables()[3].getMenuRnds()[2], restaurant.getTables()[3].getMenu()[2],
                    restaurant.getTables()[3].getMenuRnds()[3], restaurant.getTables()[3].getMenu()[3],
                    restaurant.getTables()[3].getRndLunch(), restaurant.getTables()[3].getPrettyNextEndLunch(),
                    restaurant.getTables()[3].getPrittyNextFinish(),
                    restaurant.getTables()[4].getPrittyState(),//Datos de Mesa 5
                    restaurant.getTables()[4].getMenuRnds()[0], restaurant.getTables()[4].getMenu()[0],
                    restaurant.getTables()[4].getMenuRnds()[1], restaurant.getTables()[4].getMenu()[1],
                    restaurant.getTables()[4].getMenuRnds()[2], restaurant.getTables()[4].getMenu()[2],
                    restaurant.getTables()[4].getMenuRnds()[3], restaurant.getTables()[4].getMenu()[3],
                    restaurant.getTables()[4].getRndLunch(), restaurant.getTables()[4].getPrettyNextEndLunch(),
                    restaurant.getTables()[4].getPrittyNextFinish(),
                    restaurant.getTables()[5].getPrittyState(),//Datos de Mesa 5
                    restaurant.getTables()[5].getMenuRnds()[0], restaurant.getTables()[5].getMenu()[0],
                    restaurant.getTables()[5].getMenuRnds()[1], restaurant.getTables()[5].getMenu()[1],
                    restaurant.getTables()[5].getMenuRnds()[2], restaurant.getTables()[5].getMenu()[2],
                    restaurant.getTables()[5].getMenuRnds()[3], restaurant.getTables()[5].getMenu()[3],
                    restaurant.getTables()[5].getRndLunch(), restaurant.getTables()[5].getPrettyNextEndLunch(),
                    restaurant.getTables()[5].getPrittyNextFinish()
                ,contClients, contClientsLost
                });
    }

    /**
     * Checks wich table has priority, and return the priority One.
     * Also, adds a minute to the second rated priority.
     * @param first First table to compare
     * @param second Second table to compare
     * @return The Priority One table
     */
    private int checkTablePriority(int first, int second) {
        int priorityOne = first;
        int priorityTwo = second;
        /*
         * Checks wich table has priority, considering that the
         * "DELIVERING" state got more priority than "TAKING_ORDER".
         * If the second table has the priority, then change the
         * auxiliar variables.
         */
        if (restaurant.getTables()[first].getState()
                < restaurant.getTables()[second].getState()) {
            priorityOne = second;
            priorityTwo = first;
        }
        //Finally adds the time necesary for delivering an order to the second table.
        restaurant.getTables()[priorityTwo].addDeliveringWatingTime();
        return priorityOne;
    }

    /**
     * Checks the JVM Memory state, and update the UI Information
     */
    private void checkMemoryState() {
        try {
            Runtime garbageCollector = Runtime.getRuntime();
            window.updateMemoryState(garbageCollector.totalMemory(), garbageCollector.freeMemory());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
